/*******************************************************************************
* Copyright (c) 2016 Ericsson
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors: Isaac Arvestad (Ericsson) - initial API and implementation
*******************************************************************************/
package org.eclipse.swtbot.generator.client;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.eclipse.jdt.core.ElementChangedEvent;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElementDelta;
import org.eclipse.jdt.core.IMethod;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.swt.widgets.Display;
import org.eclipse.text.edits.MalformedTreeException;
import org.eclipse.text.edits.TextEdit;
/**
* Recorder is a singleton which keeps track of recorder state information and
* the generated code received from the server.
*/
public enum Recorder implements RecorderClientCodeListener, RecorderClientStatusListener {
INSTANCE;
private RecorderClient recorderClient;
private List<RecorderClientCodeListener> codeListeners;
private List<RecorderClientStatusListener> statusListeners;
private boolean isInitialized = false;
private boolean isRecording;
private boolean isInsertingDirectlyInEditor;
private ConnectionState connectionState;
private IDocument document;
private IMethod selectedMethod;
private IDocument selectedMethodDocument;
/**
* Initialize recorder.
*/
public void initialize() {
isInitialized = true;
codeListeners = new ArrayList<RecorderClientCodeListener>();
statusListeners = new ArrayList<RecorderClientStatusListener>();
codeListeners.add(this);
statusListeners.add(this);
document = new Document();
reset();
}
/**
* Reset recording state.
*/
public void reset() {
isRecording = false;
isInsertingDirectlyInEditor = false;
selectedMethod = null;
selectedMethodDocument = null;
connectionState = ConnectionState.DISCONNECTED;
}
/**
* Start a recorder client session.
*/
public void startRecorderClient(int port) {
interruptRecorderClient();
connectionState = ConnectionState.CONNECTING;
recorderClient = new RecorderClient(codeListeners, statusListeners, port);
recorderClient.start();
}
/**
* Interrupt a recorder client session and reset recording state.
*/
public void interruptRecorderClient() {
if (recorderClient != null) {
recorderClient.interrupt();
recorderClient.closeSocket();
recorderClient = null;
}
reset();
}
/**
* Takes care of new code. If a method is selected and
* <code>isInsertingDirectlyInEditor<code> is true, add code directly to
* editor. Otherwise add it to the recorder view document.
*/
@Override
public void codeGenerated(String code) {
if (isRecording == false) {
return;
}
if (isInsertingDirectlyInEditor && selectedMethod != null && selectedMethodDocument != null) {
insertInEditor(code);
} else {
insertInView(code);
}
}
@Override
public void connectionStarted() {
connectionState = ConnectionState.CONNECTED;
}
@Override
public void connectionEnded() {
connectionState = ConnectionState.DISCONNECTED;
reset();
}
/**
* Appends a row of code to the document contained in the recorder view.
*
* @param code
* The code to append.
*/
private void insertInView(final String code) {
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
if (document.get().length() == 0) {
document.set(code + ";");
} else {
document.set(document.get() + "\n" + code + ";");
}
}
});
}
/**
* Adds a new line of code to the currently selected method and the editor
* which contains this method.
*
* @param code
* The code to append.
*/
private void insertInEditor(String code) {
ICompilationUnit methodCompilationUnit = selectedMethod.getCompilationUnit();
ASTParser parser = ASTParser.newParser(AST.JLS8);
parser.setSource(methodCompilationUnit);
parser.setResolveBindings(true);
parser.setBindingsRecovery(true);
ASTNode rootNode = parser.createAST(null);
CompilationUnit compilationUnit = (CompilationUnit) rootNode;
// Create a visitor which finds all method declarations
MethodDeclarationVisitor methodDeclarationVisitor = new MethodDeclarationVisitor();
compilationUnit.accept(methodDeclarationVisitor);
// Search for the method declaration corresponding to selectedMethod
MethodDeclaration method = methodDeclarationVisitor.findMethodDeclaration(selectedMethod);
final ASTRewrite rewrite = ASTRewrite.create(rootNode.getAST());
ListRewrite listRewrite = rewrite.getListRewrite(method.getBody(), Block.STATEMENTS_PROPERTY);
Statement statement = (Statement) rewrite.createStringPlaceholder(code + ";", ASTNode.EMPTY_STATEMENT);
listRewrite.insertLast(statement, null);
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
try {
TextEdit edits = rewrite.rewriteAST();
try {
edits.apply(selectedMethodDocument);
} catch (MalformedTreeException e) {
e.printStackTrace();
} catch (BadLocationException e) {
e.printStackTrace();
}
} catch (JavaModelException e1) {
e1.printStackTrace();
} catch (IllegalArgumentException e1) {
e1.printStackTrace();
}
};
});
}
/**
* MethodDeclarationVisitor store all MethodDeclarations it can find and
* provides functionality for finding a specific MethodDeclaration with a
* corresponding IMethod.
*/
private class MethodDeclarationVisitor extends ASTVisitor {
private List<MethodDeclaration> methodDeclarations = new ArrayList<MethodDeclaration>();
@Override
public boolean visit(MethodDeclaration node) {
methodDeclarations.add(node);
return super.visit(node);
}
/**
* Returns the MethodDeclaration from a corresponding IMethod.
*
* @param method
* The IMethod to search with.
* @return The first corresponding MethodDeclaration found, or null if
* no MethodDeclaration can be found.
*/
public MethodDeclaration findMethodDeclaration(IMethod method) {
for (MethodDeclaration methodDeclaration : methodDeclarations) {
if (methodDeclaration.resolveBinding().getJavaElement().equals(method)) {
return methodDeclaration;
}
}
return null;
}
}
/**
* Determines if UI which is related to code in the open editor needs to be
* updated. The method selection viewer should be updated if the document is
* closed or if a method is removed/added.
*
* @param event
* The received ElementChangedEvent.
* @return True if method selection viewer should be updated and false if
* not.
*/
public boolean shouldUpdateMethodSelectionViewer(ElementChangedEvent event) {
IJavaElementDelta delta = event.getDelta();
List<IJavaElementDelta> children = getAffectedLeafDeltas(delta);
for (IJavaElementDelta child : children) {
if (child.getElement() instanceof IMethod) {
return true;
} else if (child.getElement() instanceof ICompilationUnit) {
if (child.getKind() == IJavaElementDelta.CHANGED) {
return true;
}
}
}
return false;
}
/**
* Finds and returns the leaves of the change tree.
*
* @param delta
* The root to be iterated through.
* @return The leaf nodes.
*/
private List<IJavaElementDelta> getAffectedLeafDeltas(IJavaElementDelta delta) {
List<IJavaElementDelta> leaves = new ArrayList<IJavaElementDelta>();
return getAffectedLeafDeltasRecursively(delta, leaves);
}
/**
* Helper method for <code>getAffectedLeafDeltas</code> which recursively
* iterates over a IJavaElementDelta and returns the leaf nodes of the
* change tree.
*
* @param delta
* The root to be iterated through.
* @param leaves
* The leaf nodes already found.
* @return The leaf nodes found.
*/
private List<IJavaElementDelta> getAffectedLeafDeltasRecursively(IJavaElementDelta delta,
List<IJavaElementDelta> leaves) {
for (IJavaElementDelta childDelta : delta.getAffectedChildren()) {
if (childDelta.getAffectedChildren().length == 0) {
// Found a leaf!
leaves.add(childDelta);
} else {
getAffectedLeafDeltasRecursively(childDelta, leaves);
}
}
return leaves;
}
/**
* ConnectionState describes the various possible connection states between
* the recorder client and server.
*/
public enum ConnectionState {
CONNECTED, CONNECTING, DISCONNECTED;
}
public boolean isInsertingDirectlyInEditor() {
return isInsertingDirectlyInEditor;
}
public void setInsertingDirectlyInEditor(boolean isInsertingDirectlyInEditor) {
this.isInsertingDirectlyInEditor = isInsertingDirectlyInEditor;
}
public IMethod getSelectedMethod() {
return selectedMethod;
}
/**
* Sets the selected method. It is important that this method is part of the
* selected method document.
*
* @param selectedMethod
* The selected method.
*/
public void setSelectedMethod(IMethod selectedMethod) {
this.selectedMethod = selectedMethod;
}
public IDocument getSelectedMethodDocument() {
return selectedMethodDocument;
}
/**
* Sets the seletctedMethodDocument. It is important that this document
* contains the selected method.
*
* @param selectedMethodDocument
* The document that the selected method resides in.
*/
public void setSelectedMethodDocument(IDocument selectedMethodDocument) {
this.selectedMethodDocument = selectedMethodDocument;
}
/**
* Returns the text contained in the document. Use from UI thread.
*
* @return The text contained in the document.
*/
public String getDocumentText() {
return document.get();
}
/**
* Clears the document by setting the contents to an empty String. Use from
* UI thread.
*/
public void clearDocument() {
document.set("");
}
/**
* Returns the recorder document which contains recorded code. Use from UI
* thread.
*
* @return The document.
*/
public IDocument getDocument() {
return document;
}
public boolean isRecording() {
return isRecording;
}
public void setRecording(boolean isRecording) {
this.isRecording = isRecording;
}
public boolean isInitialized() {
return isInitialized;
}
public ConnectionState getConnectionState() {
return connectionState;
}
public int getPort() {
return recorderClient.getPort();
}
/**
* Add a code listener to the list of code listeners.
*
* @param listener
* The listener to add.
* @return Specified by {@link Collection#add}
*/
public boolean addCodeListener(RecorderClientCodeListener listener) {
return codeListeners.add(listener);
}
/**
* Removes the first occurrence of the specified listener from the list of
* code listeners.
*
* @param listener
* The listener to remove.
* @return Specified by {@link Collection#remove(Object)}
*/
public boolean removeCodeListener(RecorderClientCodeListener listener) {
return codeListeners.remove(listener);
}
/**
* Add a status listener to the list of status listeners.
*
* @param listener
* The listener to add.
* @return Specified by {@link Collection#add}
*/
public boolean addStatusListener(RecorderClientStatusListener listener) {
return statusListeners.add(listener);
}
/**
* Removes the first occurrence of the specified listener from the list of
* code listeners.
*
* @param listener
* The listener to remove.
* @return Specified by {@link Collection#remove(Object)}
*/
public boolean removeStatusListener(RecorderClientStatusListener listener) {
return statusListeners.remove(listener);
}
}